/*
 *
 * Copyright (c) 2007, Oracle and/or its affiliates. All rights reserved.
 * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 */
// Copyright (c) 1995-96 by Cisco Systems, Inc.

package com.sun.jmx.snmp.daemon;

// java imports
//

import java.util.Hashtable;
import java.util.logging.Level;
import java.util.Stack;
import java.net.InetAddress;
import java.net.SocketException;

// jmx imports
//
import static com.sun.jmx.defaults.JmxProperties.SNMP_ADAPTOR_LOGGER;

import com.sun.jmx.snmp.SnmpDefinitions;
import com.sun.jmx.snmp.SnmpStatusException;
import com.sun.jmx.snmp.SnmpVarBindList;

/**
 * This class is used for sending INFORM REQUESTS from an agent to a manager.
 *
 * Creates, controls, and manages one or more inform requests.
 *
 * The SnmpSession maintains the list of all active inform requests and inform responses. Each
 * SnmpSession has a dispatcher that is a thread used to service all the inform requests it creates
 * and each SnmpSession uses a separate socket for sending/receiving inform requests/responses.
 *
 * An SnmpSession object is associated with an SNMP adaptor server. It is created the first time an
 * inform request is sent by the SNMP adaptor server and is destroyed (with its associated
 * SnmpSocket) when the SNMP adaptor server is stopped.
 */

class SnmpSession implements SnmpDefinitions, Runnable {

  // PRIVATE VARIABLES
  //------------------

  /**
   * The SNMP adaptor associated with this SnmpSession.
   */
  protected transient SnmpAdaptorServer adaptor;
  /**
   * The SnmpSocket to be used to communicate with the manager
   * by all inform requests created in this session.
   */
  protected transient SnmpSocket informSocket = null;
  /**
   * This table maintains the list of inform requests.
   */
  private transient Hashtable<SnmpInformRequest, SnmpInformRequest> informRequestList =
      new Hashtable<>();
  /**
   * This table maintains the list of inform responses.
   * A FIFO queue is needed here.
   */
  private transient Stack<SnmpInformRequest> informRespq =
      new Stack<>();
  /**
   * The dispatcher that will service all inform responses to inform requests generated
   * using this session object. An SnmpSession object creates one or more inform requests.
   * Thus it services all inform requests, which are created by this session object,
   * when an inform response arrives for an inform request generated by the session.
   */
  private transient Thread myThread = null;
  /**
   * Request being synchronized from session thread. This happens when
   * a user does sync operation from a callback.
   */
  private transient SnmpInformRequest syncInformReq;

  SnmpQManager snmpQman = null;

  private boolean isBeingCancelled = false;

  // PUBLIC CONSTRUCTORS
  //--------------------

  /**
   * Constructor for creating a new session.
   *
   * @param adp The SNMP adaptor associated with this SnmpSession.
   * @throws SocketException Unable to initialize the SnmpSocket.
   */
  public SnmpSession(SnmpAdaptorServer adp) throws SocketException {
    adaptor = adp;
    snmpQman = new SnmpQManager();
    SnmpResponseHandler snmpRespHdlr = new SnmpResponseHandler(adp, snmpQman);
    initialize(adp, snmpRespHdlr);
  }

  /**
   * Constructor for creating a new session. Allows subclassing.
   */
  public SnmpSession() throws SocketException {
  }
  // OTHER METHODS
  //--------------

  /**
   * Initializes the SnmpSession.
   *
   * @param adp The SNMP adaptor associated with this SnmpSession.
   * @throws SocketException Unable to initialize the SnmpSocket.
   */
  protected synchronized void initialize(SnmpAdaptorServer adp,
      SnmpResponseHandler snmpRespHdlr)
      throws SocketException {
    informSocket = new SnmpSocket(snmpRespHdlr, adp.getAddress(), adp.getBufferSize().intValue());

    myThread = new Thread(this, "SnmpSession");
    myThread.start();
  }

  /**
   * Indicates whether the thread for this session is active and the SNMP adaptor server ONLINE.
   *
   * @return true if active, false otherwise.
   */
  synchronized boolean isSessionActive() {
    //return ((myThread != null) && (myThread.isAlive()));
    return ((adaptor.isActive()) && (myThread != null) && (myThread.isAlive()));
  }

  /**
   * Gets the SnmpSocket which will be used by inform requests created in this session.
   *
   * @return The socket which will be used in this session.
   */
  SnmpSocket getSocket() {
    return informSocket;
  }

  /**
   * Gets the SnmpQManager which will be used by inform requests created in this session.
   *
   * @return The SnmpQManager which will be used in this session.
   */
  SnmpQManager getSnmpQManager() {
    return snmpQman;
  }

  /**
   * Indicates whether this session is performing synchronous operation for an inform request.
   *
   * @return <CODE>true</CODE> if the session is performing synchronous operation,
   * <CODE>false</CODE> otherwise.
   */
  private synchronized boolean syncInProgress() {
    return syncInformReq != null;
  }

  private synchronized void setSyncMode(SnmpInformRequest req) {
    syncInformReq = req;
  }

  private synchronized void resetSyncMode() {
    if (syncInformReq == null) {
      return;
    }
    syncInformReq = null;
    if (thisSessionContext()) {
      return;
    }
    this.notifyAll();
  }

  /**
   * Returns <CODE>true</CODE> if the current executing thread is this session's dispatcher.
   * Typically used to detect whether the user is doing a sync operation from
   * this dispatcher context. For instance, a user gives a sync command
   * from within a request callback using its associated session.
   *
   * @return <CODE>true</CODE> if current thread is this session's dispatcher, <CODE>false</CODE>
   * otherwise.
   */
  boolean thisSessionContext() {
    return (Thread.currentThread() == myThread);
  }

  /**
   * Sends an inform request to the specified InetAddress destination using the specified community
   * string.
   *
   * @param addr The InetAddress destination for this inform request.
   * @param cs The community string to be used for the inform request.
   * @param cb The callback that is invoked when a request is complete.
   * @param vblst A list of SnmpVarBind instances or null.
   * @throws SnmpStatusException SNMP adaptor is not ONLINE or session is dead.
   */
  SnmpInformRequest makeAsyncRequest(InetAddress addr, String cs,
      SnmpInformHandler cb,
      SnmpVarBindList vblst, int port)
      throws SnmpStatusException {

    if (!isSessionActive()) {
      throw new SnmpStatusException("SNMP adaptor server not ONLINE");
    }
    SnmpInformRequest snmpreq = new SnmpInformRequest(this, adaptor, addr, cs, port, cb);
    snmpreq.start(vblst);
    return snmpreq;
  }

  /**
   * Performs sync operations on active requests. Any number of inform requests
   * can be done in sync mode but only one per thread.
   * The user can do synchronous operation using the request handle only.
   */
  void waitForResponse(SnmpInformRequest req, long waitTime) {

    if (!req.inProgress()) {
      return;
    }
    setSyncMode(req);
    if (SNMP_ADAPTOR_LOGGER.isLoggable(Level.FINEST)) {
      SNMP_ADAPTOR_LOGGER.logp(Level.FINEST, SnmpSession.class.getName(),
          "waitForResponse",
          "Session switching to sync mode for inform request " + req.getRequestId());
    }
    long maxTime;
    if (waitTime <= 0) {
      maxTime = System.currentTimeMillis() + 6000 * 1000;
    } else {
      maxTime = System.currentTimeMillis() + waitTime;
    }

    while (req.inProgress() || syncInProgress()) {
      waitTime = maxTime - System.currentTimeMillis();
      if (waitTime <= 0) {
        break;
      }
      synchronized (this) {
        if (!informRespq.removeElement(req)) {
          try {
            this.wait(waitTime);
          } catch (InterruptedException e) {
          }
          continue;
        }
      }
      try {
        processResponse(req);
      } catch (Exception e) {
        if (SNMP_ADAPTOR_LOGGER.isLoggable(Level.FINEST)) {
          SNMP_ADAPTOR_LOGGER.logp(Level.FINEST, SnmpSession.class.getName(),
              "waitForResponse", "Got unexpected exception", e);
        }
      }
    }
    resetSyncMode();
  }

  /**
   * Dispatcher method for this session thread. This is the dispatcher method
   * which goes in an endless-loop and waits for servicing inform requests
   * which received a reply from the manager.
   */
  @Override
  public void run() {
    myThread = Thread.currentThread();
    myThread.setPriority(Thread.NORM_PRIORITY);

    SnmpInformRequest reqc = null;
    while (myThread != null) {
      try {
        reqc = nextResponse();
        if (reqc != null) {
          processResponse(reqc);
        }
      } catch (ThreadDeath d) {
        myThread = null;
        if (SNMP_ADAPTOR_LOGGER.isLoggable(Level.FINEST)) {
          SNMP_ADAPTOR_LOGGER.logp(Level.FINEST, SnmpSession.class.getName(),
              "run", "ThreadDeath, session thread unexpectedly shutting down");
        }
        throw d;
      }
    }
    if (SNMP_ADAPTOR_LOGGER.isLoggable(Level.FINER)) {
      SNMP_ADAPTOR_LOGGER.logp(Level.FINER, SnmpSession.class.getName(),
          "run", "Session thread shutting down");
    }
    myThread = null;
  }

  private void processResponse(SnmpInformRequest reqc) {

    while (reqc != null && myThread != null) {
      try {
        if (SNMP_ADAPTOR_LOGGER.isLoggable(Level.FINER)) {
          SNMP_ADAPTOR_LOGGER.logp(Level.FINER, SnmpSession.class.getName(),
              "processResponse", "Processing response to req = " + reqc.getRequestId());
        }
        reqc.processResponse();  // Handles out of memory.
        reqc = null;  // finished processing.
      } catch (Exception e) {
        if (SNMP_ADAPTOR_LOGGER.isLoggable(Level.FINEST)) {
          SNMP_ADAPTOR_LOGGER.logp(Level.FINEST, SnmpSession.class.getName(),
              "processResponse", "Got unexpected exception", e);
        }
        reqc = null;
      } catch (OutOfMemoryError ome) {
        if (SNMP_ADAPTOR_LOGGER.isLoggable(Level.FINEST)) {
          SNMP_ADAPTOR_LOGGER.logp(Level.FINEST, SnmpSession.class.getName(),
              "processResponse", "Out of memory error in session thread", ome);
        }
        Thread.yield();
        continue;   // re-process the request.
      }
    }
  }

  // HANDLING INFORM REQUESTS LIST AND INFORM RESPONSES LIST
  //--------------------------------------------------------

  /**
   * Adds an inform request.
   *
   * @param snmpreq The inform request to add.
   * @throws SnmpStatusException SNMP adaptor is not ONLINE or session is dead.
   */
  synchronized void addInformRequest(SnmpInformRequest snmpreq) throws SnmpStatusException {

    // If the adaptor is not ONLINE, stop adding requests.
    //
    if (!isSessionActive()) {
      throw new SnmpStatusException("SNMP adaptor is not ONLINE or session is dead...");
    }
    informRequestList.put(snmpreq, snmpreq);
  }

  /**
   * Deletes an inform request.
   *
   * @param snmpreq The inform request to delete.
   */
  synchronized void removeInformRequest(SnmpInformRequest snmpreq) {
    // deleteRequest can be called from destroySnmpSession.
    //In such a case remove is done in cancelAllRequest method.
    if (!isBeingCancelled) {
      informRequestList.remove(snmpreq);
    }

    if (syncInformReq != null && syncInformReq == snmpreq) {
      resetSyncMode();
    }
  }

  /**
   * Cancels all pending inform requests in this session.
   */
  private void cancelAllRequests() {
    final SnmpInformRequest[] list;

    synchronized (this) {

      if (informRequestList.isEmpty()) {
        return;
      }

      isBeingCancelled = true;

      list = new SnmpInformRequest[informRequestList.size()];
      java.util.Iterator<SnmpInformRequest> it = informRequestList.values().iterator();
      int i = 0;
      while (it.hasNext()) {
        SnmpInformRequest req = it.next();
        list[i++] = req;
        it.remove();
      }
      informRequestList.clear();
    }

    for (int i = 0; i < list.length; i++) {
      list[i].cancelRequest();
    }
  }

  /**
   * Adds the inform request object which received a response to an inform request
   * generated by the session.  This is added to a private store, which
   * will be eventually picked up by the dispatcher for processing.
   *
   * @param reqc The inform request that received the response from the manager.
   */
  void addResponse(SnmpInformRequest reqc) {

    SnmpInformRequest snmpreq = reqc;
    if (isSessionActive()) {
      synchronized (this) {
        informRespq.push(reqc);
        this.notifyAll();
      }
    } else {
      if (SNMP_ADAPTOR_LOGGER.isLoggable(Level.FINEST)) {
        SNMP_ADAPTOR_LOGGER.logp(Level.FINEST, SnmpSession.class.getName(),
            "addResponse",
            "Adaptor not ONLINE or session thread dead, so inform response is dropped..." + reqc
                .getRequestId());
      }
    }
  }

  private synchronized SnmpInformRequest nextResponse() {

    if (informRespq.isEmpty()) {
      try {
        if (SNMP_ADAPTOR_LOGGER.isLoggable(Level.FINER)) {
          SNMP_ADAPTOR_LOGGER.logp(Level.FINER, SnmpSession.class.getName(),
              "nextResponse", "Blocking for response");
        }
        this.wait();
      } catch (InterruptedException e) {
      }
    }
    if (informRespq.isEmpty()) {
      return null;
    }
    SnmpInformRequest reqc = informRespq.firstElement();
    informRespq.removeElementAt(0);
    return reqc;
  }

  private synchronized void cancelAllResponses() {
    if (informRespq != null) {
      syncInformReq = null;
      informRespq.removeAllElements();
      this.notifyAll();
    }
  }

  /**
   * Destroys any pending inform requests and then stops the session.
   * The session will not be usable after this method returns.
   */
  final void destroySession() {

    cancelAllRequests();
    cancelAllResponses();
    synchronized (this) {
      informSocket.close();
      informSocket = null;
    }
    snmpQman.stopQThreads();
    snmpQman = null;
    killSessionThread();
  }

  /**
   * Make sure you are killing the thread when it is active. Instead
   * prepare for a graceful exit.
   */
  private synchronized void killSessionThread() {

    if ((myThread != null) && (myThread.isAlive())) {
      if (SNMP_ADAPTOR_LOGGER.isLoggable(Level.FINER)) {
        SNMP_ADAPTOR_LOGGER.logp(Level.FINER, SnmpSession.class.getName(),
            "killSessionThread", "Destroying session");
      }
      if (!thisSessionContext()) {
        myThread = null;
        this.notifyAll();
      } else {
        myThread = null;
      }
    }
  }

  /**
   * Finalizer of the <CODE>SnmpSession</CODE> objects.
   * This method is called by the garbage collector on an object
   * when garbage collection determines that there are no more references to the object.
   * <P>Removes all the requests for this SNMP session, closes the socket and
   * sets all the references to the <CODE>SnmpSession</CODE> object to <CODE>null</CODE>.
   */
  @Override
  protected void finalize() {

    if (SNMP_ADAPTOR_LOGGER.isLoggable(Level.FINER)) {
      SNMP_ADAPTOR_LOGGER.logp(Level.FINER, SnmpSession.class.getName(),
          "finalize", "Shutting all servers");
    }

    if (informRespq != null) {
      informRespq.removeAllElements();
    }
    informRespq = null;
    if (informSocket != null) {
      informSocket.close();
    }
    informSocket = null;

    snmpQman = null;
  }

}
